package com.symaster.common.optimization;

import com.symaster.common.thread.WatchThread;
import com.symaster.common.util.ArrayUtils;
import com.symaster.common.util.IOUtil;
import com.symaster.common.util.ProbabilityUtil;

import java.io.Closeable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author yinmiao
 * @version 2022-06-01 16:43
 */
public class Heredity implements Closeable {

    private final ThreadPoolExecutor threadPoolExecutor;
    private final int[] initG;
    private final int concurrencyLevel;
    // private final int dfSize, dps, mutation;
    private Operator operator;
    private UpdateFinListener updateFinListener;
    private StopListener stopListener;
    private final Config config;
    private final Queue<int[]> queue;
    private final Set<String> geneSet;
    private final List<Cores> cores;
    private final List<FractionObj> fractionObjs;
    private final Statistics statistics;

    public Heredity(int[] initG) {
        this(initG, 4);
    }

    /**
     * @param initG  初始基因
     * @param concurrencyLevel 并发数量
     */
    public Heredity(int[] initG, int concurrencyLevel) {
        this(initG, concurrencyLevel, Config.ofDefault());
    }

    public Heredity(int[] initG, int concurrencyLevel, Config config) {
        this.initG = initG;
        this.concurrencyLevel = Math.max(concurrencyLevel, 4);
        this.config = config;
        this.queue = new LinkedBlockingQueue<>();
        this.cores = new ArrayList<>();
        this.fractionObjs = new LinkedList<>();
        this.geneSet = new HashSet<>();
        for (int i = 0; i < this.concurrencyLevel; i++) {
            this.cores.add(new RunCore(this));
        }
        this.cores.add(new GeneGenerate(this));
        this.statistics = new Statistics();
        this.threadPoolExecutor = new ThreadPoolExecutor(concurrencyLevel + (concurrencyLevel >> 1), 600, 3 * 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.DiscardOldestPolicy());
        this.threadPoolExecutor.allowCoreThreadTimeOut(true);
    }

    public void start() {
        if (this.operator == null) {
            throw new IllegalArgumentException("operator is null");
        }
        if (running()) {
            return;
        }
        this.cores.forEach(Cores::start);
    }

    private boolean running() {
        long count1 = this.cores.stream().filter(Cores::runnable).count();
        return count1 > 0;
    }

    public FractionObj stop() throws TimeoutException {
        if (!running()) {
            return null;
        }
        cores.forEach(WatchThread::watchStop);

        FractionObj fractionObj;
        synchronized (fractionObjs) {
            if (fractionObjs.isEmpty()) {
                return null;
            }

            fractionObjs.sort((o1, o2) -> Double.compare(o2.fraction, o1.fraction));
            fractionObj = fractionObjs.get(0);
        }

        if (stopListener != null) {
            threadPoolExecutor.execute(() -> stopListener.handler(fractionObj.gene, fractionObj.fraction, statistics.count));
        }
        fractionObj.setCount(statistics.count);
        return fractionObj;
    }

    public void setStopListener(StopListener stopListener) {
        this.stopListener = stopListener;
    }

    public void setOperator(Operator operator) {
        if (running()) {
            throw new IllegalArgumentException("running");
        }
        this.operator = operator;
    }

    public void setUpdateFinListener(UpdateFinListener listener) {
        this.updateFinListener = listener;
    }

    private int[] copy(int[] arr) {
        int[] result = new int[arr.length];
        System.arraycopy(arr, 0, result, 0, arr.length);
        return result;
    }

    @Override
    public void close() throws IOException {
        try {
            stop();
        } catch (TimeoutException e) {
            throw new IOException("TimeOut");
        } finally {
            release();
        }
    }

    private void release() {
        try {
            cores.forEach(WatchThread::stop);
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        try {
            IOUtil.close(cores.toArray(new WatchThread[0]));
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        try {
            this.threadPoolExecutor.shutdown();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        try {
            queue.clear();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        try {
            geneSet.clear();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        try {
            cores.clear();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        try {
            fractionObjs.clear();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
    }

    public interface Operator {
        Result handler(int[] g) throws Exception;
    }

    public interface UpdateFinListener {
        void handler(int[] g, double fin);
    }

    public interface StopListener {
        void handler(int[] g, double e, int count);
    }

    public static abstract class Cores extends WatchThread {
        protected final Heredity heredity;

        public Cores(Heredity heredity) throws IllegalArgumentException {
            super(0, "heredity-" + UUID.randomUUID());
            this.heredity = heredity;
        }

        @Override
        protected void startEvent() {
        }

        @Override
        protected void stopEvent(int o) {
        }

        @Override
        protected boolean errorHandler(Throwable e) {
            e.printStackTrace(System.err);
            return false;
        }
    }

    public static class Config {
        private long timeout = 10;
        private TimeUnit timeoutUnit = TimeUnit.SECONDS;
        private int initialQuantity = 50;
        public double mutationRate = 0.05;
        public double heritability = 0.8;

        public static Config of(long timeout, TimeUnit timeoutUnit) {
            Config config = new Config();
            config.timeout = timeout;
            config.timeoutUnit = timeoutUnit;
            return config;
        }

        public static Config of(long timeout, TimeUnit timeoutUnit, int initialQuantity) {
            Config config = new Config();
            config.timeout = timeout;
            config.timeoutUnit = timeoutUnit;
            config.initialQuantity = initialQuantity;
            return config;
        }

        public static Config of(long timeout, TimeUnit timeoutUnit, int initialQuantity, double mutationRate) {
            Config config = new Config();
            config.timeout = timeout;
            config.timeoutUnit = timeoutUnit;
            config.initialQuantity = initialQuantity;
            config.mutationRate = mutationRate;
            return config;
        }

        public static Config ofDefault() {
            return new Config();
        }
    }

    public static class FractionObj implements Comparable<FractionObj> {
        private final int[] gene;
        private final double fraction;
        private Object obj;
        private int count = 0;

        public FractionObj(int[] gene, double fraction) {
            this(gene, fraction, null);
        }

        public FractionObj(int[] gene, double fraction, Object obj) {
            this.gene = gene;
            this.fraction = fraction;
            this.obj = obj;
        }

        public static FractionObj of(int[] gene, double fraction) {
            return new FractionObj(gene, fraction);
        }

        public static FractionObj of(int[] gene, double fraction, Object obj) {
            return new FractionObj(gene, fraction, obj);
        }

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

        public Object getObj() {
            return obj;
        }

        public void setObj(Object obj) {
            this.obj = obj;
        }

        public int[] getGene() {
            return gene;
        }

        public double getFraction() {
            return fraction;
        }

        @Override
        public String toString() {
            return "FractionObj{" +
                    "gene=" + Arrays.toString(gene) +
                    ", fraction=" + fraction +
                    '}';
        }

        @Override
        public int compareTo(FractionObj o) {
            return Double.compare(o.fraction, fraction);
        }
    }

    public static class GeneGenerate extends Cores {

        public GeneGenerate(Heredity heredity) throws IllegalArgumentException {
            super(heredity);
        }

        @Override
        protected void execute() throws Exception {
            if (super.heredity.queue.size() < heredity.config.initialQuantity / 2) {
                generate();
            }
        }

        private void generate() {
            synchronized (heredity.fractionObjs) {
                FractionObj forward = null;
                FractionObj back = null;
                if (!heredity.fractionObjs.isEmpty()) {
                    forward = heredity.fractionObjs.get(0);
                    heredity.fractionObjs.sort((o1, o2) -> Double.compare(o2.fraction, o1.fraction));
                    back = heredity.fractionObjs.get(0);
                }
                if (heredity.updateFinListener != null && back != null && forward != null && back != forward && back.fraction > forward.fraction) {
                    FractionObj finalBack = back;
                    heredity.threadPoolExecutor.execute(() -> heredity.updateFinListener.handler(finalBack.gene, finalBack.fraction));
                }
            }

            if (heredity.fractionObjs.isEmpty()) {
                initGenerate();
            } else {
                myGenerate();
            }
        }

        private void myGenerate() {
            for (int i = 0; i < heredity.config.initialQuantity; i++) {
                FractionObj fractionObj = heredity.fractionObjs.get(heredity.fractionObjs.size() < 3 ? 0 : new Random().nextInt(3));
                // FractionObj fractionObj = heredity.fractionObjs.get(0);
                if (ProbabilityUtil.exeProbability(heredity.config.mutationRate)) {
                    addGeneQueue(ArrayUtils.getRandomArrays(fractionObj.gene));
                } else {
                    int[] copy = heredity.copy(fractionObj.gene);
                    int count = Math.min(Math.max((int) Math.round((1 - heredity.config.heritability) * copy.length / 2.0), 1), copy.length - 1);
                    ArrayUtils.random(copy, count);
                    addGeneQueue(copy);
                }
            }
        }

        private void addGeneQueue(int[] randomArrays) {
            String string = Arrays.toString(randomArrays);
            if (!heredity.geneSet.contains(string)) {
                heredity.queue.add(randomArrays);
                heredity.geneSet.add(string);
            }
        }

        private void initGenerate() {
            for (int i = 0; i < heredity.config.initialQuantity; i++) {
                int[] randomArrays = ArrayUtils.getRandomArrays(heredity.initG);
                heredity.queue.add(randomArrays);
            }
        }
    }

    public static class RunCore extends Cores {

        public RunCore(Heredity heredity) throws IllegalArgumentException {
            super(heredity);
        }

        @Override
        protected void execute() throws Exception {
            int[] gene = heredity.queue.poll();
            if (gene != null) {

                Result handler = heredity.operator.handler(gene);
                FractionObj of = FractionObj.of(gene, handler.fraction, handler.obj);
                synchronized (heredity.fractionObjs) {
                    heredity.fractionObjs.add(of);
                }
            }
            heredity.statistics.countOnce();
        }
    }

    public static class Statistics {
        private int count = 0;

        public synchronized void countOnce() {
            count++;
        }

        public int getCount() {
            return count;
        }
    }

    public static class Result {
        private final double fraction;
        private Object obj;

        public Result(double fraction) {
            this(fraction, null);
        }

        public Result(double fraction, Object obj) {
            this.fraction = fraction;
            this.obj = obj;
        }

        public double getFraction() {
            return fraction;
        }

        public Object getObj() {
            return obj;
        }

        public void setObj(Object obj) {
            this.obj = obj;
        }
    }
}
